package hydrap2p.protocol.bittorrent;

import hydrap2p.download.DownloadProtocol;
import hydrap2p.download.Downloader;
import hydrap2p.download.Source;
import hydrap2p.helper.ProcessOutput;
import hydrap2p.helper.ServiceConnector;
import hydrap2p.helper.ThriftServerThread;
import hydrap2p.helper.ThriftUtils;
import hydrap2p.library.Library;
import hydrap2p.library.Range;
import hydrap2p.logging.Level;
import hydrap2p.logging.Logging;
import hydrap2p.modulemanager.ModuleControl;
import hydrap2p.servicemanager.NameTaken;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.BindException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.TreeMap;

import org.apache.log4j.PropertyConfigurator;
import org.apache.thrift.TException;
import org.klomp.snark.Snark;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

/**
 * Implementation of BitTorrent for HydraP2P.
 * 
 * @author Phil
 */
public class BTProtocol implements DownloadProtocol.Iface, ModuleControl.Iface {
	static {
		//Configure log4j
		Properties log4jProps = new Properties();
		log4jProps.setProperty("log4j.rootLogger", "TRACE, console");
		log4jProps.setProperty("log4j.appender.console", "org.apache.log4j.ConsoleAppender");
		log4jProps.setProperty("log4j.appender.console.layout", "org.apache.log4j.PatternLayout");
		log4jProps.setProperty("log4j.appender.console.layout.ConversionPattern", "%-4r [%t] %-5p %c %x - %m%n");

		PropertyConfigurator.configure(log4jProps);
	}
	
	// Class string name, doubling as logging marker
	private static final Marker TAG = MarkerFactory.getDetachedMarker("BTProtocol");

	// Console logger - used before connections established
	private static final Logger consoleLogger = LoggerFactory.getLogger(TAG.getName());

	private static ThriftServerThread t;
	
	private static Map<Long, Process> btProcesses = new TreeMap<Long, Process>();

	private int port;
	
	/**
	 * Instantiates a new bT protocol.
	 * 
	 * @param port the port
	 */
	public BTProtocol(int port) {
		this.port = port;
	}

	/**
	 * The main method.
	 * 
	 * @param args the arguments
	 */
	public static void main(String[] args) {
		// Tell Manager about BTProtocol's existence
		int port = 34657;
		BTProtocol btProtocol = null;

		try {
			btProtocol = new BTProtocol(port);
		} catch(Exception e) {
			System.out.println("Failed to start BTProtocol, " + e.getMessage());
			return;
		}

		// Start server
		consoleLogger.info(TAG, "Starting BT server");
		
		try {
			t = ThriftUtils.initThriftServer(new DownloadProtocol.Processor(btProtocol), "BTProtocol", port);
		} catch (BindException e) {
			consoleLogger.error(TAG, "Failed to start BTProtocol!  Could not bind to port.");
			e.printStackTrace();
			return;
		} catch (IOException e) {
			consoleLogger.error(TAG, "Failed to start BTProtocol!");
			e.printStackTrace();
			return;
		} catch (NameTaken e) {
			consoleLogger.error(TAG, "Failed to start BTProtocol!  Duplicate module exists.");
			e.printStackTrace();
			return;
		}
		
		//Ensure module connections
		boolean manager = ThriftUtils.waitForManager();
		boolean library = ThriftUtils.waitForLibrary();
		boolean downloader = ThriftUtils.waitForDownloader();
		boolean logging = ThriftUtils.waitForLogging();
		
		if(manager && library && downloader) {
			try {
				Downloader.Client downloaderModule = ServiceConnector.getDownloader();
				downloaderModule.addDownloadProtocol(t.getLocation(), "bittorrent");
			} catch (TException e) {
				consoleLogger.error(TAG, "Failed to register with downloader!");
				return;
			}
			
			t.start();
		}
		else
			consoleLogger.error(TAG, "Failed to connect to essential modules!  Exiting.");
	}
	
	/* (non-Javadoc)
	 * @see hydrap2p.download.DownloadProtocol.Iface#cancelDownload(long, hydrap2p.download.Source)
	 */
	@Override
	public void cancelDownload(long uid, Source source) throws TException {
		Process p = btProcesses.get(uid);
		
		if(p != null) {
			p.destroy();
		}
	}
	
	public void download(long uid, Source source, Range range, Downloader.Iface downloader, Library.Iface library)
	{
		byte[] torrentFile = null;
		String downloadPath = "";
		File tempFile = null;
		
		try {
			log(Level.DEBUG, "Download starting - BT");
			if(source == null || source.getProtocol() == null) {
				downloader.erroredDownloading(uid, source, range, true);
				log(Level.ERROR, "Invalid source passed to BT!");
				return;
			} else if(!source.getProtocol().equalsIgnoreCase("bittorrent")) {
				downloader.erroredDownloading(uid, source, range, true);
				log(Level.ERROR, "Source with incorrect protocol passed to BT!");
				return;
			} else if(source.getExtra() == null) {
				downloader.erroredDownloading(uid, source, range, true);
				log(Level.ERROR, "Source with null extras passed to BT!");
				return;
			} else if(!source.getAttachment().containsKey("torrent_file")) {
				downloader.erroredDownloading(uid, source, range, true);
				log(Level.ERROR, "Source with no torrent_file attachment passed to BT!");
				return;
			} 

			torrentFile = source.getAttachment().get("torrent_file").array();
			downloadPath = source.getExtra().get("path");
			
			//Ensure download path is not nothing
			if(downloadPath == null || downloadPath.equals("")) {
				log(Level.ERROR, "Invalid download path: " + downloadPath);
				downloader.erroredDownloading(uid, source, range, true);
				return;
			}
			
			//Write torrent file data to temporary file 
			Random rnd = new Random();
			byte[] randomBytes = new byte[6];
			rnd.nextBytes(randomBytes);
			
			//Cast the bytes to strings, creating a random name
			String randomName = "";
			for(byte b : randomBytes) {
				randomName += b;
			}
			
			tempFile = File.createTempFile(randomName, ".torrent");
			tempFile.deleteOnExit();
			
			//Write data into tempfile
			FileOutputStream out = new FileOutputStream(tempFile);
			out.write(torrentFile);
			out.flush();
			out.close();
		}
		catch (TException e)
		{
			log(Level.ERROR, "A communication error happened with a required module!");
			return;
		} catch(IOException e) {
			e.printStackTrace();
		} 

		if(tempFile == null) {
			log(Level.ERROR, "Could not create temporary file!");
			return;
		}
			
		Process process = null;
		ProcessOutput output = null;
		ProcessOutput error = null;

		try {
			String downloadDir = "";
			
			//FIXME: There's probably a better way to do this
			if(downloadPath.contains("/") && downloadPath.contains("\\")) {
				int bsIndex = downloadPath.lastIndexOf("\\");
				int fsIndex = downloadPath.lastIndexOf("/");
			
				downloadDir = downloadPath.substring(0, Math.max(bsIndex, fsIndex));
			} else if(downloadPath.contains("/")) {
				downloadDir = downloadPath.substring(0, downloadPath.lastIndexOf("/"));
			} else if(downloadPath.contains("\\")) {
				downloadDir = downloadPath.substring(0, downloadPath.lastIndexOf("\\"));
			} else  {
				log(Level.ERROR, "Failed to parse download path!");
				return;
			}
			
			String currentDir = System.getProperty("user.dir");
			System.out.println("Current working directory: " + currentDir);
			System.out.println("Download directory: " + downloadDir);
			
			//Runtime.exec is used here because there is no way to change working directory, and 
			//Snark does not support a download directory argument - it uses its working directory 
			//FIXME: BTProtocol will probably not work if java.exe is not in the user's PATH.  Do this with installer?
			process = Runtime.getRuntime().exec("java -jar \"" + currentDir + "/lib/snark-hydrap2p.jar\" --debug \"" + tempFile.getAbsolutePath() + "\"", null, new File(downloadDir));
			output = new ProcessOutput(process.getInputStream());
			error = new ProcessOutput(process.getErrorStream());
		
			//Must grab the streams of the executing process immediately, or exec() will hang
			output.start();
			error.start();
			
			btProcesses.put(uid, process);
		} catch(IOException e) {
			e.printStackTrace();
		}
		
		return;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.DownloadProtocol.Iface#download(long, hydrap2p.download.Source, hydrap2p.library.Range)
	 */
	@Override
	public void download(long uid, Source source, Range range) throws TException {
		//TODO Wait for each and check for validity
		Downloader.Client downloader = ServiceConnector.getDownloader();
		Library.Client library = ServiceConnector.getLibrary();
		download(uid, source, range, downloader, library);
	}

	/* (non-Javadoc)
	 * @see hydrap2p.Service.Iface#ping()
	 */
	@Override
	public void ping() throws TException {

	}

	/* (non-Javadoc)
	 * @see hydrap2p.Service.Iface#supportedInterfaces()
	 */
	@Override
	public List<String> supportedInterfaces() throws TException {
		return Arrays.asList("DownloadProtocol", "ModuleControl");
	}

	/**
	 * Log.
	 * 
	 * @param severity the severity
	 * @param logMessage the log message
	 */
	protected void log(Level severity, String logMessage) {
		Logging.Client logging = ServiceConnector.getLogger();
		
		//Log to Logging module if possible, and fall back to console.
		try {
			logging.log(severity, TAG.toString(), logMessage);
		} catch(TException e) {
			logToSystemError(severity, logMessage);
		} catch(NullPointerException e) {
			logToSystemError(severity, logMessage);
		}
	}

	/**
	 * Log to system error.
	 * 
	 * @param severity the severity
	 * @param logMessage the log message
	 */
	private void logToSystemError(Level severity, String logMessage) { 
		//Log to system error.  If they can't be arsed to use a Logging constant as severity, the message gets logged at trace level
		switch(severity) {
		case VERBOSE:
			consoleLogger.trace(TAG, logMessage);
			break;
		case DEBUG:
			consoleLogger.debug(TAG, logMessage);
			break;
		case INFO:
			consoleLogger.info(TAG, logMessage);
			break;
		case MISHAP:
			consoleLogger.warn(TAG, logMessage);
			break;
		case ERROR:
			consoleLogger.error(TAG, logMessage);
			break;
		default:
			consoleLogger.trace(TAG, logMessage);
		}
	}

	/* (non-Javadoc)
	 * @see hydrap2p.manager.ModuleControl.Iface#stop()
	 */
	@Override
	public void stop() throws TException {
		consoleLogger.info(TAG, "Stopping BT server");
		
		t.shutdown();
	}
}
